//                 _              
//                | |             
//   __ _ _ __ ___| |__ __  _____ 
//  / _` | '__/ __| '_ \\ \/ / __|
// | (_| | | | (__| | | |>  <\__ \
//  \__,_|_|  \___|_| |_/_/\_\___/
//                                
// Copyright 2010 Scott Dixon http://www.archxs.com
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package com.archxs.commons;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.security.Permission;
import java.util.Locale;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

/**
 * Unit tests to cover the {@link VariantObjectCacheFactory} object.
 */
public class VariantObjectCacheFactoryTest {

    /**
     * Be nice and restore any impl property value set elsewhere.
     */
    private volatile String m_exisitingImplProperty;
    
    /**
     * Be safe and restore any security manager previously set.
     */
    private volatile SecurityManager m_existingSecurityManager;
    
    /**
     * Same treatment for the debug property.
     */
    private volatile boolean m_debugFactory;
    
    @Before
    public void setupTest() {
        m_exisitingImplProperty = System.getProperty(VariantObjectCache.CACHE_IMPL_PROPERTY);
        m_debugFactory = Boolean.getBoolean(VariantObjectCache.CACHE_DEBUG_PROPERTY);
        m_existingSecurityManager = System.getSecurityManager();
    }
    
    @After
    public void tearDownTest() {
        System.setSecurityManager(m_existingSecurityManager);
        if (null != m_exisitingImplProperty && m_exisitingImplProperty.length() > 0) {
            System.setProperty(VariantObjectCache.CACHE_IMPL_PROPERTY, m_exisitingImplProperty);
        } else {
            System.clearProperty(VariantObjectCache.CACHE_IMPL_PROPERTY);
        }
        if (!m_debugFactory) {
            System.clearProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY);
        } else {
            System.setProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY, Boolean.TRUE.toString());
        }
    }
    
    /**
     * Used to test reimplementing the object cache.
     */
    public static class VariantObjectCacheTest implements VariantObjectCache {

        public static volatile boolean m_throwMockException = false;
        
        public VariantObjectCacheTest() {
            if (m_throwMockException) {
                throw new RuntimeException("Fake constructor exception.");
            }
        }
        
        // +-------------------------------------------------------------------+
        // | VariantObjectCache
        // +-------------------------------------------------------------------+
        @Override
        public void clearCache() {
        }

        @Override
        public int getCacheSize() {
            return 0;
        }

        @Override
        public Variant getCachedValue(final String value,
                                      final Locale locale) {
            return null;
        }

        @Override
        public Variant getCachedValue(final String[] value,
                                      final Locale locale) {
            return null;
        }

        @Override
        public Variant getCachedValue(final String value) {
            return null;
        }

        @Override
        public Variant getCachedValue(final String[] value) {
            return null;
        }
        
        @Override
        public Variant readThroughCache(final Class<? extends Variant> type, final String value) {
            return null;
        }

        @Override
        public Variant readThroughCache(final Class<? extends Variant> type, final String value, final Locale locale) {
            return null;
        }

        @Override
        public Variant readThroughCache(final Class<? extends Variant> type, final String[] value) {
            return null;
        }

        @Override
        public Variant readThroughCache(final Class<? extends Variant> type, final String[] value, final Locale locale) {
            return null;
        }

        @Override
        public void setDebug(final boolean debugOnOff) {
        }
        
    };
    
    public static class NotAnObjectCache {
        
    };
    
    // +-----------------------------------------------------------------------+
    // | UNIT TESTS
    // +-----------------------------------------------------------------------+
    @Test
    public void testCreateDefaultFactory() {
        System.clearProperty(VariantObjectCache.CACHE_IMPL_PROPERTY);
        Assert.assertNull(System.getProperty(VariantObjectCache.CACHE_IMPL_PROPERTY));
        final VariantObjectCache defaultCache = callPrivateFactoryMethod();
        Assert.assertTrue(String.format("Unknown default object cache implementation %s", defaultCache.getClass().getName()), defaultCache instanceof VariantObjectCacheDefault);
    }
    
    @Test
    public void testStableGetInstance() {
        Assert.assertSame(VariantObjectCacheFactory.getInstance(), VariantObjectCacheFactory.getInstance());
    }
    
    @Test
    public void testBadClassName() {
        System.setProperty(VariantObjectCache.CACHE_IMPL_PROPERTY, "bad.class.Value");
        // Try w/ quiet failures
        System.setProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY, Boolean.toString(false));
        Assert.assertTrue(callPrivateFactoryMethod() instanceof VariantObjectCacheDefault);
        System.setProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY, Boolean.toString(true));
        Assert.assertTrue(callPrivateFactoryMethod() instanceof VariantObjectCacheDefault);
    }

    @Test
    public void testEmptyClassName() {
        System.setProperty(VariantObjectCache.CACHE_IMPL_PROPERTY, "");
        // Try w/ quiet failures
        System.setProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY, Boolean.toString(false));
        Assert.assertTrue(callPrivateFactoryMethod() instanceof VariantObjectCacheDefault);
        System.setProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY, Boolean.toString(true));
        Assert.assertTrue(callPrivateFactoryMethod() instanceof VariantObjectCacheDefault);
    }
    
    @Test
    public void testProvideNewObjectFactory() {
        System.setProperty(VariantObjectCache.CACHE_IMPL_PROPERTY, VariantObjectCacheTest.class.getName());
        System.setProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY, Boolean.TRUE.toString());
        Assert.assertEquals(VariantObjectCacheTest.class, callPrivateFactoryMethod().getClass());
        System.setProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY, Boolean.FALSE.toString());
        Assert.assertEquals(VariantObjectCacheTest.class, callPrivateFactoryMethod().getClass());
    }
    
    @Test
    public void testProvideNewObjectFactoryThatThrowsCtorException() {
        VariantObjectCacheTest.m_throwMockException = true;
        try {
            System.setProperty(VariantObjectCache.CACHE_IMPL_PROPERTY, VariantObjectCacheTest.class.getName());
            System.setProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY, Boolean.TRUE.toString());
            Assert.assertEquals(VariantObjectCacheDefault.class, callPrivateFactoryMethod().getClass());
            System.setProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY, Boolean.FALSE.toString());
            Assert.assertEquals(VariantObjectCacheDefault.class, callPrivateFactoryMethod().getClass());
        } finally {
            VariantObjectCacheTest.m_throwMockException = false;
        }
    }
    
    @Test
    public void testProvideNewObjectFactoryThatIsnt() {
        System.setProperty(VariantObjectCache.CACHE_IMPL_PROPERTY, NotAnObjectCache.class.getName());
        System.setProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY, Boolean.TRUE.toString());
        Assert.assertEquals(VariantObjectCacheDefault.class, callPrivateFactoryMethod().getClass());
        System.setProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY, Boolean.FALSE.toString());
        Assert.assertEquals(VariantObjectCacheDefault.class, callPrivateFactoryMethod().getClass());
    }
    
    @Test(expected=UnsupportedOperationException.class)
    public void testFactorySelfDefence() throws Throwable {
        final Constructor<?> defaultCtor = VariantObjectCacheFactory.class.getDeclaredConstructor();
        defaultCtor.setAccessible(true);
        try {
            defaultCtor.newInstance();
        } catch (final InvocationTargetException e) {
            throw e.getCause();
        }
    }
    
    @Test
    public void testCacheImplKeyReadNotAllowed() {
        System.setProperty(VariantObjectCache.CACHE_IMPL_PROPERTY, VariantObjectCacheTest.class.getName());
        System.setSecurityManager(new SecurityManager() {
            @Override
            public void checkPermission(final Permission perm) {
                if (perm.getName().equals(VariantObjectCache.CACHE_IMPL_PROPERTY)) {
                    throw new SecurityException();
                }
            }
        });
        System.setProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY, Boolean.TRUE.toString());
        Assert.assertEquals(VariantObjectCacheDefault.class, callPrivateFactoryMethod().getClass());
        System.setProperty(VariantObjectCache.CACHE_DEBUG_PROPERTY, Boolean.FALSE.toString());
        Assert.assertEquals(VariantObjectCacheDefault.class, callPrivateFactoryMethod().getClass());
    }
    
    @Test
    public void testDebugKeyReadNotAllowed() {
        System.setSecurityManager(new SecurityManager() {
            @Override
            public void checkPermission(final Permission perm) {
                if (perm.getName().equals(VariantObjectCache.CACHE_DEBUG_PROPERTY)) {
                    throw new SecurityException();
                }
            }
        });
        // simply make sure no exceptions are thrown by debug being disallowed.
        Assert.assertEquals(VariantObjectCacheDefault.class, callPrivateFactoryMethod().getClass());
    }
    // +-----------------------------------------------------------------------+
    // | PRIVATE
    // +-----------------------------------------------------------------------+
    private VariantObjectCache callPrivateFactoryMethod() {
        final String factoryMethodName = "createNewInstance";
        try {
            final Method createMethod = VariantObjectCacheFactory.class.getDeclaredMethod(factoryMethodName);
            createMethod.setAccessible(true);
            return (VariantObjectCache) createMethod.invoke(null);
        } catch (final Exception e) {
            e.printStackTrace();
            Assert.fail(String.format("Failed to invoke private method VariantObjectCacheFactory.%s",factoryMethodName));
            throw new RuntimeException(e);
        }
    }
}
